一進 Redux Toolkit 的官網,就會看到以下示意圖。
它最初是為了解決使用 Redux 的三個常見問題
會希望若是能像 create-react-app
一樣,有一些開箱即用的工具、Library 及樣板代碼,就能夠讓開發者更專注於開發核心邏輯。
在這之後的文章將簡稱 Redux Toolkit 為 RTK。
直接把 --template 指定為 redux,就會內含有 Redux Toolkit
npx create-react-app my-app --template redux
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
功用和 createStore 一樣可以建立 Store,結合 reducers、middleware。
createStore 是傳入參數的順序來加入 reducer 及 middleware。
configureStore 則是用 Options 形式來設定,程式碼看起來會更清楚。
若沒有指定 middleware,RTK 預設使用的是 redux-thunk。
-import { createStore } from "redux";
+import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';
- const store = createStore(rootReducer);
+ const store = configureStore({ reducer: rootReducer });
建立 action creator 的函式。放在 createAction() 裡面的參數會自動變成 action type 字串常數。
import { createAction } from '@reduxjs/toolkit';
const fetchTodos = createAction('todos/fetchTodos');
// { type: 'todos/fetchTodos' }
const setFilter = createAction('filter/setFilter');
// setFilter('All')
// returns { type: 'filter/setFilter', payload: 'All' }
使用它在撰寫 reducer 的時候可以不用再用 switch case 語法,此外,它會自動使用 immerjs 讓您更簡單的處理狀態更新,例如 state.todos[3].completed = true
。 (沒有使用 immutable 相關套件時,這樣的寫法會有 side-effect)。
import { createAction, createReducer } from '@reduxjs/toolkit';
const setFilter = createAction('filter/setFilter');
const initialState = 'All';
const filterReducer = createReducer(initialState, (builder) => {
builder
.addCase(setFilter, (state, action) => {
state = action.payload;
});
})
這個函式是 RTK 能有效率的開發 Redux 的重點,詳細說明如下。
createSlice 將 slice name、initial state、reducer、action 集中建立,在slice 檔案中。
createSlice 內部整合了 createReducer 和 createAction,因此 在大部分應用中, 不需特別寫這二個函式,只要使用 createSlice 就足夠。
import { createSlice } from '@reduxjs/toolkit';
const initialState = 'All';
const filterSlice = createSlice({
name: 'filter',
initialState,
reducers: {
setFilter(state, action) {
state = action.payload
},
},
});
export const { setFilter } = filterSlice.actions;
export default filterSlice.reducer;
createSlice() 接收一個 Options 作為參數
createSlice({
name: ...
initialState: ...
reducers: {
// 一般 reducer 定義的地方
setFilter(state, action) {
state = action.payload
},
},
extraReducers: (builder) => {
// 加入額外 reducer 的地方,使用 createAction 或 createAsyncThunk 建立的 action creators 都會設定在此
},
});
createSlice 生成的 reducer 也會被稱為
case reducers
,它對應於之前寫法的 switch case,透過 createSlice 制作 reducer 後,就不需要再做 switch case。這裡沒有 default 處理函數。createSlice 生成的 reducer 會自動返回當前的 state。
沒有使用 RTK 以前,都要避免變動到當前的 state,你必須小心的處理及回傳新的 state 物件,或是套用 immutable 相關的套件。
createSlice 預設使用 Immer 套件處理,它把函數用 Immer 裡的 produce 封裝了起來。這意味著你可以寫任何修改 reducer 裡面的狀態的代碼,而 Immer 會安全地返回一個被正確地更新過的結果。
如果想要更深入理解原理,可以看看 RTK 官方的這篇說明,之後也會做為鐵人賽的補充,試著翻譯看看。
slice 使用 createSlice 建立完成後,createSlice 會自動生成 action creators 及 reducer,就可以直接使用或是導出。
export const { setFilter } = filterSlice.actions;
export default filterSlice.reducer;
使用 RTK 創造出來 action creators 只接受一個名稱為 palyload 的參數。如果 payload 是單一值,直接使用該值做為 payload 的全部。如果 payload 有多個值,payload 就是一個含有所有值的一個 Object。
用以處理非同步,接受一個 action type string 和一個返回 promise 的函數,並生成一個發起基於該 promise 的 pending/fulfilled/rejected action 類型的 thunk。
後面實作串接 API 時,會再深入介紹,現在先知道 RTK 有 createAsyncThunk 用以處理非同步。
改寫 前面的 Redux 範例,Fork 出來一份,把 Todo MVC 改成使用 Redux Toolkit。
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers";
const store = configureStore({ reducer: rootReducer });
export default store;
這邊因為後面想加上 loading 的效果,所以 state 改用物件形式。
// store/slices/todosSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
data: [],
isLoading: false,
error: false
};
const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {
addTodo(state, action) {
const { id, text } = action.payload;
state.data.push({ id, text, completed: false });
},
toggleTodo(state, action) {
const id = action.payload;
const todo = state.data.find((todo) => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo(state, action) {
const id = action.payload;
state.data = state.data.filter((todo) => {
return todo.id !== id;
});
}
}
});
export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;
// store/slices/filterSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = "All";
const filterSlice = createSlice({
name: "filter",
initialState,
reducers: {
setFilter(state, action) {
state = action.payload;
return state;
}
}
});
export const { setFilter } = filterSlice.actions;
export default filterSlice.reducer;
Tips:如果 payload 是單值的話,state 若直接指為 payload 記得 reuders function 一定回傳 state,否則會出現錯誤訊息如下圖。
// reducers/index.js
import { combineReducers } from "redux";
import todos from "./slices/todosSlice";
import filter from "./slices/filterSlice";
const reducers = combineReducers({
todos,
filter,
});
export default reducers;
受益於 createSlice,將 slice name、initial state、reducer、action 集中建立在一個檔案,action 及 reducer 目錄都可以移掉。
在這邊我們也先移除 middleware 目錄,後面會再介紹 RTK 的用法。
變更 import { your-action-creators} from slices/xxxSlice
。
- import { setFilter, fetchTodosAsync } from "../store/actions";
+ import { setFilter } from "../store/slices/filterSlice";
// 為了加上 loading 小小調整一下 useSelector
- const todos = useSelector((state) => state.todos);
+ const todos = useSelector((state) => state.todos.data);
...
// 暫時註解 dispatch(fetchTodos());
+ onClick={() => {
+ // dispatch(fetchTodos());
+ }}
- import { addTodo } from "../store/actions";
+ import { addTodo } from "../store/slices/todosSlice";
// 這裡小小調整一下,原本 id 自動產生邏輯是寫在 action creators 裡
- dispatch(addTodo(inputRef.current.value));
+ dispatch(
+ addTodo({
+ id: new Date().getTime().toString(),
+ text: inputRef.current.value
+ })
+);
- import { toggleTodo, deleteTodo } from "../store/actions";
+ import { toggleTodo, deleteTodo } from "../store/slices/todosSlice";
// 為了加上 loading 小小調整一下 useSelector
- const todos = useSelector((state) => state.todos);
+ const todos = useSelector((state) => state.todos.data);
完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-toolkit-nk0nsh
// api/todos.js
import axios from "axios";
const fetchTodos = () => {
return axios.get("https://jsonplaceholder.typicode.com/users/1/todos");
};
export default { fetchTodos };
// api/index.js
import todosAPI from "./todos";
export { todosAPI };
// components/TodoList.js
...
const isLoading = useSelector((state) => state.todos.isLoading);
...
// 加上有 Loading 區塊的元件
const TodoListWithLoading = () => {
if (isLoading) {
return (
<div style={{ padding: "10px", fontSize: "20px", textAlign: "center" }}>
Loading...
</div>
);
}
return (
<ul className="todo-list">
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggleItem={() => dispatch(toggleTodo(todo.id))}
onDeleteItem={() => dispatch(deleteTodo(todo.id))}
/>
))}
</ul>
);
};
...
// jsx 使用 <TodoListWithLoading />
return (
<>
<section className="main">
<TodoListWithLoading />
</section>
</>
);
createAsyncThunk
接受二個參數
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { todosAPI } from "../../api";
// 這邊要導出,因為它不會納至 todoSlice.actions 中
export const fetchTodos = createAsyncThunk("todos/fetchTodos", async () => {
const response = await todosAPI.fetchTodos();
return response;
});
使用 createAsyncThunk 建立的非同步 action creators,就會自動產生下面的對應
// promise pending 等待中
fetchTodos.pending(); // action type => 'todos/fetchTodos/pending'
// promise fulfilled 正確完成
fetchTodos.fulfilled(); // action type => 'todos/fetchTodos/fulfilled'
// promise reject 已拒絕,操作失敗
fetchTodos.rejected(); // action type => 'todos/fetchTodos/rejected'
const todosSlice = createSlice({
name: "todos",
initialState,
reducers: {
// 一般 reducer fuction 定義的地方
...
},
// 加入額外 reducer 的地方
// 使用 createAction 或 createAsyncThunk 建立的 action creators 都會設定在此
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state, action) => {
state.isLoading = true;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.data = action.payload.data.map(({ id, title, completed }) => {
return {
id,
text: title,
completed
};
});
state.isLoading = false;
});
}
import { fetchTodos } from "../store/slices/todosSlice";
...
// 把之前的註解取消掉
<span
style={{ zIndex: 10 }}
onClick={() => {
dispatch(fetchTodos());
}}
>
Load Online Todos
</span>
完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-toolkit-api-b4ob97
RTK 官方提供更簡單的方式去操作 API,可以先看看官方的 RTK Query 說明,之後也會做為鐵人賽的補充。
Redux Toolkit (RTK) 幫我們簡化了使用 Redux 時的複雜操作步驟,接下來要介紹的 zustand 則是可以取代/加強 Redux 的狀態管理套件。它是可擴張式的狀態管理解決方案,讓你透過 hook 的方式直覺的採用狀態管理。
https://ithelp.ithome.com.tw/articles/10275089
https://github.com/DeerTeam/redux-toolkit-in-chinese
https://redux-toolkit-cn.netlify.app/
https://pjchender.dev/react/redux-toolkit/